package com.wei.czz.framework.common.helper;

import com.wei.czz.common.constant.Constant;
import com.wei.czz.common.dto.common.limitConfig.LimitStatsDto;
import com.wei.czz.common.enums.ResultEnum;
import com.wei.czz.common.enums.common.limitConfig.LimitConfigTypeEnum;
import com.wei.czz.common.exception.CzzException;
import com.wei.czz.common.po.LimitConfigPo;
import com.wei.czz.common.po.SlideWindowPo;
import com.wei.czz.framework.common.entity.LimitConfigEntity;
import com.wei.czz.framework.common.service.LimitConfigService;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Created by IntelliJ IDEA.
 *
 * @author wei
 * date: 2023-02-18 21:55:40
 * className: LimitHelper 系统限流服务接口
 * version: 1.0
 * description:
 */
@Service
@AllArgsConstructor
public class LimitHelper {

    private static final Logger log = LoggerFactory.getLogger(LimitHelper.class);

    /**
     * 接口与滑动窗口映射表
     */
    private static final Map<String, SlideWindowPo> map = new HashMap<>();
    /**
     * 公平锁
     * 后续操作映射表数据时，需要先锁定，单线程操作
     */
    private static final ReentrantLock mapLock = new ReentrantLock(true);

    private final LimitConfigService limitConfigService;

    /**
     * 初始化系统限流规则操作
     */
    public void init() {
        log.info("开始系统限流规则初始化");
        List<LimitConfigEntity> limitConfigList = limitConfigService.queryUsableLimitConfigList();
        if (limitConfigList.isEmpty()) {
            log.warn("没有查询到限流配置数据");
            return;
        }
        // 取相同限流配置的最高版本数据
        Map<String, LimitConfigEntity> limitConfigMap = limitConfigList.stream()
                .collect(Collectors.toMap(
                        limitConfig -> limitConfig.getType() + Constant.SPLIT + limitConfig.getRequestKey(),
                        Function.identity(),
                        (o1, o2) -> o1.getVersion().compareTo(o2.getVersion()) > 0 ? o1 : o2
                ));

        long now = System.currentTimeMillis();

        for (LimitConfigEntity limitConfig : limitConfigMap.values()) {
            LimitConfigTypeEnum typeEnum = LimitConfigTypeEnum.get(limitConfig.getType());
            if (typeEnum == null) {
                log.warn("未知限流配置。{}", limitConfig);
                continue;
            }
            SlideWindowPo slideWindowPo = map.computeIfAbsent(limitConfig.getRequestKey(), k -> new SlideWindowPo());
            LimitConfigPo limitConfigPo = new LimitConfigPo();
            // 得到限流统计时长
            long milliSecond = getMilliSecond(limitConfig.getTimeNum(), limitConfig.getTimeType());
            // 滑动窗口列表
            Deque<Integer> slideWindow = new LinkedList<>();

            limitConfigPo.setMaxNum(limitConfig.getMaxNum())
                    .setWindowNum(limitConfig.getWindowNum())
                    .setWindowSpace(milliSecond / limitConfig.getWindowNum().longValue())
                    .setWindowTime(now + limitConfigPo.getWindowSpace())
                    .setLimitTime(limitConfig.getLimitTime() * 1000)
                    .setMessage(limitConfig.getMessage())
                    .setTotalCount(0)
                    .setSlideWindow(slideWindow);
            // 滑动窗口列表填充
            this.fullSlideWindow(slideWindow, limitConfigPo.getWindowNum());

            if (LimitConfigTypeEnum.REQUEST_TYPE == typeEnum) {
                slideWindowPo.setRequestNode(limitConfigPo);
            } else if (LimitConfigTypeEnum.EXCEPTION_TYPE == typeEnum) {
                slideWindowPo.setFailNode(limitConfigPo);
            }
        }
        log.info("系统限流规则初始化执行完成");
    }

    /**
     * 新增限流规则数据操作
     * @param limitConfig 限流配置参数对象
     */
    public void addLimitRule(LimitConfigEntity limitConfig) {
        long now = System.currentTimeMillis();

        // 得到限流统计时长
        long milliSecond = getMilliSecond(limitConfig.getTimeNum(), limitConfig.getTimeType());

        SlideWindowPo slideWindowPo = map.get(limitConfig.getRequestKey());
        if (slideWindowPo == null) {
            // 加锁
            mapLock.lock();
            try {
                // 再次尝试获取
                slideWindowPo = map.get(limitConfig.getRequestKey());
                if (slideWindowPo == null) {
                    // double check
                    slideWindowPo = new SlideWindowPo();
                }
                // 滑动窗口列表
                Deque<Integer> slideWindow = new LinkedList<>();

                LimitConfigPo limitConfigPo = new LimitConfigPo();

                limitConfigPo.setMaxNum(limitConfig.getMaxNum())
                        .setWindowNum(limitConfig.getWindowNum())
                        .setWindowSpace(milliSecond / limitConfig.getWindowNum().longValue())
                        .setWindowTime(now + limitConfigPo.getWindowSpace())
                        .setLimitTime(limitConfig.getLimitTime() * 1000)
                        .setMessage(limitConfig.getMessage())
                        .setTotalCount(0)
                        .setSlideWindow(slideWindow);
                // 滑动窗口列表填充
                this.fullSlideWindow(slideWindow, limitConfigPo.getWindowNum());

                if (LimitConfigTypeEnum.REQUEST_TYPE.getValue().equals(limitConfig.getType())) {
                    slideWindowPo.setRequestNode(limitConfigPo);
                } else if (LimitConfigTypeEnum.EXCEPTION_TYPE.getValue().equals(limitConfig.getType())) {
                    slideWindowPo.setFailNode(limitConfigPo);
                } else {
                    //
                    log.error("限流类型未知。type={}", limitConfig.getType());
                }
                // 记录新的限流配置映射
                map.put(limitConfig.getRequestKey(), slideWindowPo);
            } finally {
                // 解锁
                mapLock.unlock();
            }
            // 终止往下执行
            return;
        }

        LimitConfigPo limitConfigPo = null;
        if (LimitConfigTypeEnum.REQUEST_TYPE.getValue().equals(limitConfig.getType())) {
            limitConfigPo = slideWindowPo.getRequestNode();
            if (limitConfigPo == null) {
                // 新请求限流配置
                limitConfigPo = new LimitConfigPo();
                slideWindowPo.setRequestNode(limitConfigPo);
            }
        } else if (LimitConfigTypeEnum.EXCEPTION_TYPE.getValue().equals(limitConfig.getType())) {
            limitConfigPo = slideWindowPo.getFailNode();
            if (limitConfigPo == null) {
                // 新异常限流配置
                limitConfigPo = new LimitConfigPo();
                slideWindowPo.setFailNode(limitConfigPo);
            }
        }
        if (limitConfigPo == null) {
            // 限流配置对象为空，不往下进行
            // 此处判断为无用判断，没有实际效果
            return;
        }

        ReentrantLock lock = slideWindowPo.getLock();
        // 加锁
        lock.lock();
        try {
            // 比较新旧滑动窗口大小
            long num = limitConfig.getWindowNum() - limitConfigPo.getWindowNum();

            // 使用最新的值
            limitConfigPo.setMaxNum(limitConfig.getMaxNum())
                    .setWindowNum(limitConfig.getWindowNum())
                    .setWindowSpace(milliSecond / limitConfigPo.getWindowNum())
                    .setLimitTime(limitConfig.getLimitTime() * 1000)
                    .setMessage(limitConfig.getMessage());
            if (limitConfigPo.getWindowTime() == 0) {
                // 第一个窗口对应的时间戳为初始值，需要更新为最新值
                limitConfigPo.setWindowTime(now + limitConfigPo.getWindowSpace());
            }
            if (num != 0) {
                // 滑动窗口 -> 新旧长度不一致，需要动态调整滑动窗口长度
                Deque<Integer> _slideWindow = limitConfigPo.getSlideWindow();
                if (num > 0) {
                    // 新长度大于旧长度，向滑动窗口头部添加新窗口节点
                    for (int i = 0; i < num; i++) {
                        _slideWindow.addFirst(0);
                    }
                } else {
                    // 新长度小于旧长度，去除滑动窗口头部窗口节点
                    num = -num;
                    int totalCount = limitConfigPo.getTotalCount();
                    for (int i = 0; i < num; i++) {
                        Integer count = _slideWindow.pollFirst();
                        if (count != null && count != 0) {
                            totalCount -= count;
                        }
                    }
                    limitConfigPo.setTotalCount(totalCount);
                }
            }
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    /**
     * 去除限流规则数据操作
     * @param requestKey 接口路径
     * @param type       限流类型
     */
    public void deleteLimitRule(String requestKey, Integer type) {
        SlideWindowPo slideWindowPo = map.get(requestKey);
        if (slideWindowPo != null) {
            // 加锁
            mapLock.lock();
            try {
                ReentrantLock lock = slideWindowPo.getLock();
                // 加锁
                lock.lock();
                try {
                    if (LimitConfigTypeEnum.REQUEST_TYPE.getValue().equals(type)) {
                        slideWindowPo.setRequestNode(null);
                    } else if (LimitConfigTypeEnum.EXCEPTION_TYPE.getValue().equals(type)) {
                        slideWindowPo.setFailNode(null);
                    } else {
                        log.warn("未知限流类型。type{}", type);
                        return;
                    }
                } finally {
                    // 解锁
                    lock.unlock();
                }
                if (ObjectUtils.allNull(slideWindowPo.getRequestNode(), slideWindowPo.getFailNode())) {
                    // 清理滑动窗口对象
                    map.remove(requestKey);
                }
            } finally {
                // 解锁
                mapLock.unlock();
            }
        }
    }

    /**
     * 请求限流计算操作
     * @param requestKey 接口路径
     */
    public void requestMath(String requestKey) {
        SlideWindowPo slideWindowPo = this.getOrDefault(requestKey);
        if (Objects.isNull(slideWindowPo)) {
            log.info("没有配置该请求键对应的滑动窗口配置。requestKey={}", requestKey);
            return;
        }
        // 限流计算
        slideWindowPo.requestLimitMath();
    }

    /**
     * 异常限流计算操作
     * @param requestKey 接口路径
     */
    public void failMath(String requestKey) {
        SlideWindowPo slideWindowPo = this.getOrDefault(requestKey);
        if (slideWindowPo != null) {
            // 异常限流计算
            slideWindowPo.failLimitMath();
        }
    }

    /**
     * 获取限流统计数据操作
     * @param requestKey 接口路径
     * @param type       限流类型
     * @return 限流统计数据
     */
    public LimitStatsDto getLimitStats(String requestKey, Integer type) {

        SlideWindowPo slideWindowPo = map.get(requestKey);
        if (slideWindowPo == null) {
            log.info("滑动窗口数据不存在");
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "限流统计数据不存在，请确认。");
        }
        LimitConfigPo limitConfigPo = null;
        if (LimitConfigTypeEnum.REQUEST_TYPE.getValue().equals(type)) {
            limitConfigPo = slideWindowPo.getRequestNode();
        } else if (LimitConfigTypeEnum.EXCEPTION_TYPE.getValue().equals(type)) {
            limitConfigPo = slideWindowPo.getFailNode();
        } else {
            log.warn("未知限流类型");
        }
        if (limitConfigPo == null) {
            log.info("限流统计数据不存在");
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "限流统计数据不存在，请确认。");
        }
        LimitStatsDto limitStatsDto = new LimitStatsDto();
        if (slideWindowPo.getUnLockTime() != null) {
            limitStatsDto.setUnLockTIme(new Date(slideWindowPo.getUnLockTime()));
        }
        limitStatsDto.setWindowSpace(limitConfigPo.getWindowSpace())
                .setWindowTime(new Date(limitConfigPo.getWindowTime()))
                .setTotalCount(limitConfigPo.getTotalCount())
                .setSlideWindow(limitConfigPo.getSlideWindow());
        return limitStatsDto;
    }

    /**
     * 获取接口路径对应的滑动窗口对象
     * @param requestKey 接口路径
     * @return 滑动窗口对象
     */
    public SlideWindowPo get(String requestKey) {
        return map.get(requestKey);
    }

    /**
     * 获取接口路径对应的滑动窗口对象
     * 如果接口路径对应的滑动窗口对象为空，则取默认配置
     * @param requestKey 接口路径
     * @return 滑动窗口对象
     */
    private SlideWindowPo getOrDefault(String requestKey) {
        // 正常获取
        SlideWindowPo slideWindowPo = map.get(requestKey);
        if (slideWindowPo == null) {
            // 取默认限流配置
            slideWindowPo = map.get(Constant.RATE_LIMIT_DEFAULT);
            if (slideWindowPo == null) {
                // 没有默认限流配置
                return null;
            }
            // 加锁
            mapLock.lock();
            // 再次尝试正常获取
            SlideWindowPo _slideWindowPo = map.get(requestKey);
            if (_slideWindowPo != null) {
                // double check 双重校验
                return _slideWindowPo;
            }
            try {
                // 将默认配置转化为该接口的配置
                _slideWindowPo = new SlideWindowPo();

                long now = System.currentTimeMillis();

                LimitConfigPo requestNode = slideWindowPo.getRequestNode();
                if (requestNode != null) {
                    // 滑动窗口列表
                    Deque<Integer> slideWindow = new LinkedList<>();

                    LimitConfigPo _requestNode = new LimitConfigPo();
                    _requestNode.setMaxNum(requestNode.getMaxNum())
                            .setWindowNum(requestNode.getWindowNum())
                            .setWindowSpace(requestNode.getWindowSpace())
                            .setWindowTime(now + requestNode.getWindowSpace())
                            .setLimitTime(requestNode.getLimitTime())
                            .setMessage(requestNode.getMessage())
                            .setTotalCount(0)
                            .setSlideWindow(slideWindow);
                    // 滑动窗口列表填充
                    this.fullSlideWindow(slideWindow, requestNode.getWindowNum());

                    _slideWindowPo.setRequestNode(_requestNode);
                }
                LimitConfigPo failNode = slideWindowPo.getFailNode();
                if (failNode != null) {
                    // 滑动窗口列表
                    Deque<Integer> slideWindow = new LinkedList<>();

                    LimitConfigPo _failNode = new LimitConfigPo();
                    _failNode.setMaxNum(failNode.getMaxNum())
                            .setWindowNum(failNode.getWindowNum())
                            .setWindowSpace(failNode.getWindowSpace())
                            .setWindowTime(now + failNode.getWindowSpace())
                            .setLimitTime(failNode.getLimitTime())
                            .setMessage(failNode.getMessage())
                            .setTotalCount(0)
                            .setSlideWindow(slideWindow);
                    // 滑动窗口列表填充
                    this.fullSlideWindow(slideWindow, failNode.getWindowNum());

                    _slideWindowPo.setFailNode(_failNode);
                }
                map.put(requestKey, _slideWindowPo);
            } finally {
                // 解锁
                mapLock.unlock();
            }
        }
        return slideWindowPo;
    }

    /**
     * 获取限流统计时长（单位：毫秒）
     * @param timeNum  时间数量
     * @param timeType 时间单位
     * @return 毫秒值
     */
    private long getMilliSecond(long timeNum, int timeType) {
        int i = 0;
        if (timeType > i++) {
            // 分
            timeNum *= 60;
        }
        if (timeType > i++) {
            // 时
            timeNum *= 60;
        }
        if (timeType > i++) {
            // 天
            timeNum *= 24;
        }
        Calendar calendar = Calendar.getInstance();
        if (timeType > i++) {
            // 月
            timeNum *= (calendar.get(Calendar.MONTH) + 1);
        }
        if (timeType > i) {
            // 年
            timeNum *= calendar.getActualMaximum(Calendar.DAY_OF_YEAR);
        }
        timeNum *= 1000;
        return timeNum;
    }

    /**
     * 填充滑动窗口链表
     * @param slideWindow 链表对象
     * @param size        填充个数
     */
    private void fullSlideWindow(Deque<Integer> slideWindow, Long size) {
        while (slideWindow.size() < size) {
            slideWindow.add(0);
        }
    }

}
